This notebook describes the grey radiation model and goes through some examples of things that can be done with it.
The basis of this model is to produce the vertically one dimensional structure of the atmosphere given a concentration distribution of greenhouse gases.
I used chapters 10 and 14 of Atmospheric Circulation Dynamics and General Circulation Models and chapter 4 of Principles of Planetary Climate to help produce the model.
The total energy of the atmosphere is given by: \begin{equation} \rho e^{tot} = \frac{1}{2}\rho \pmb{v}^2 + \rho \Phi + \rho u \label{eq: 1}\tag{1} \end{equation} This is the sum of kinetic energy, gravitational potential energy ($\Phi = gz$) and internal energy. There is no source of total energy, meaning it is conserved: \begin{equation} \frac{\partial}{\partial t}(\rho e^{etot}) + \nabla \cdot \pmb{F}^{etot} = 0 \label{eq: 2}\tag{2} \end{equation} Where $\textbf{F}^{etot}$ is the flux density vector of total energy determined by the the balance equations for each component of $e^{tot}$.
\begin{equation} \frac{\partial \rho}{\partial t} + \nabla \cdot (\rho \pmb{v}) = 0 \label{eq: 3}\tag{3} \end{equation}\begin{equation} \frac{d\pmb{v}}{dt} + 2\pmb{\Omega} \times \pmb{v} = -\frac{1}{\rho}\nabla p - \nabla \Phi \label{eq: 4}\tag{4} \end{equation}Using $(2)$ along with conservation of mass $(3)$ and conservation of momentum $(4)$ as well as the fact $\frac{\partial \Phi}{\partial t}=0$, we can obtain an equation for the evolution of kinetic and potential energy in flux form (note that $\frac{d}{dt} = \frac{\partial}{\partial t} + \pmb{v} \cdot \nabla$):
\begin{equation} \frac{\partial}{\partial t}(\frac{1}{2}\rho \pmb{v}^2) + \nabla \cdot \left[ \left(\frac{1}{2}\rho \pmb{v}^2 + p\right)\pmb{v} \right] = p \nabla \cdot \pmb{v} - \rho \cdot \nabla \Phi \label{eq: 5}\tag{5} \end{equation}\begin{equation} \frac{\partial}{\partial t}(\rho \Phi) + \nabla \cdot \left(\rho \Phi \pmb{v}\right) = \rho \pmb{v}\cdot \nabla \Phi \label{eq: 6}\tag{6} \end{equation}We can determine the internal energy equation by noting that the right hand sides (RHS) of $(5)$ and $(6)$ are source terms of energy so the source term for the internal energy equation must be $-p \nabla \cdot \pmb{v}$ so the source term of total energy (sum of RHS of $(5)$, $(6)$ and $(7)$) equals $0$ as to satisfy equation $(2)$. Thus the internal energy equation is:
\begin{equation} \frac{\partial}{\partial t}(\rho u) + \nabla \cdot \left(\rho u \pmb{v} + \pmb{F}\right) = -p \nabla \cdot \pmb{v} \label{eq: 7}\tag{7} \end{equation}$\pmb{F}$ is the flux density of internal energy other than the advection term i.e. it is the heat flux. In the grey radiation model, we assume the only contribution is from radiation (as opposed to sensible heat - molecular thermal diffusion).
We want to use all these equations to get an expression for how temperature, $T$, responds to changes in $\pmb{F}$. We can combine $(7)$ and $(3)$ to give:
\begin{equation} \rho \left(\frac{du}{dt} + p\frac{dv_s}{dt}\right) = -\nabla \cdot \pmb{F} \label{eq: 8}\tag{8} \end{equation}Where $v_s = 1/ \rho$ is specific volume. If we then use the definition of enthalpy, $dh = du + d(pv_s)$, we get an expression for the evolution of enthalpy:
\begin{equation} \rho \frac{dh}{dt} = \frac{dp}{dt} - \nabla \cdot \pmb{F} \label{eq: 9}\tag{9} \end{equation}We can then use $h = c_pT$ where $c_p$ is the heat capacity of air at constant pressure per unit mass (units are J/K/kg). We are also only interested in a 1D vertical ($\nabla \rightarrow \frac{\partial}{\partial z}$), static ($\pmb{v} \rightarrow 0$) atmosphere in this section. Under these conditions, $\frac{d}{dt} \rightarrow \frac{\partial}{\partial t}$, $(3)$ gives $\rho = constant$, $(4)$ gives $\frac{\partial p}{\partial t} = 0$ and $\frac{\partial}{\partial z} = -g\rho \frac{\partial}{\partial p}$. This gives our final temperature equation:
\begin{equation} \frac{\partial T}{\partial t} = \frac{g}{c_p}\frac{\partial F}{\partial p} \label{eq: 10}\tag{10} \end{equation}In the model, we discretize this using a finite volume method (pressure and flux are at the boundaries of a cell whereas temperature is at the cell centre):
\begin{equation} \frac{T^{n+1}_{i+1/2} - T^{n}_{i+1/2}}{\Delta t} = \frac{g}{c_p}\frac{F^{n}_{i+1} - F^{n}_i}{p_{i+1} - p_i} \label{eq: 11}\tag{11} \end{equation}Where $i$ indicates grid position and $n$ indicates time.
Now to complete the equation, we want to find an expression for $F$ as a function of $T$ and $p$.
Lets consider a slab of atmosphere of thickness $\Delta z$. Let there be an upwelling flux, $F^{\uparrow}_{\nu}(z)$ at the bottom of the slab and an upwelling flux $F^{\uparrow}_{\nu}(z+\Delta z)$ at the top. There is a change in flux because the layer of atmosphere absorbs and emits radiation. According to Kirchoff's law, emissivity = absorptivity = $e_{\nu}$ and we have:
\begin{equation} F^{\uparrow}_{\nu}(z+\Delta z) = (1-e_{\nu})F^{\uparrow}_{\nu}(z) + e_{\nu}\pi B(\nu, T) \label{eq: 12}\tag{12} \end{equation}This means that for each frequency $\nu$, the radiation flux that reaches the top of this slab of atmosphere is equal to the amount that was incident upon the bottom of the slab minus the amount absorbed by the slab plus the amount emitted by the slab. We assume local thermodynamic equilibrium so the emitted radiation follows the blackbody distribution, $B(\nu, T)$.
The emissivity is given by $e_{\nu} = \rho (z)k(\nu, z)\Delta z$. $\rho$ is the density of the fluid ($kgm^{-3}$). $k$ is an absorption coefficient and has units of area per unit mass ($m^2kg^{-1}$). It can be thought of as the area taken out of the incident beam by the absorbers contained in a unit mass of atmosphere. For a fluid with n different absorbing substances, $k = \sum_{i=0}^n k_iq_i$ where $q_i(z)$ is the specific humidity ($q_i = \rho_i / \rho$).
If we sub this into $(12)$, and take the limit $\Delta z \rightarrow 0$:
\begin{equation} \frac{dF^{\uparrow}_{\nu}}{dz} = -\rho kF^{\uparrow}_{\nu} + \rho k\pi B(\nu, T) \label{eq: 13}\tag{13} \end{equation}If we now define optical depth, $\tau_{\nu}(z)$ such that $d\tau_{\nu} = \rho kdz$, we get:
\begin{equation} \frac{dF^{\uparrow}_{\nu}}{d\tau_{\nu}} = -F^{\uparrow}_{\nu} + \pi B(\nu, T) \label{eq: 14}\tag{14} \end{equation}Optical depth increases as you move down through the atmosphere, coming from the surface ($\tau_{\nu} = 0$ at the surface and $\tau = \tau^{\infty}$ at the top of the atmosphere), and can be thought of as the effective distance travelled by light at a particular frequency given the interaction with the gas. The more the gas interacts with the light, the greater the optical depth. Going through the same analysis but considering the downwelling radiation incident upon the top of the slab, we get:
\begin{equation} F^{\downarrow}_{\nu}(z-\Delta z) = (1-e_{\nu})F^{\downarrow}_{\nu}(z) + e_{\nu}\pi B(\nu, T) \label{eq: 15}\tag{15} \end{equation}which becomes:
\begin{equation} \frac{dF^{\downarrow}_{\nu}}{d\tau_{\nu}} = F^{\downarrow}_{\nu} - \pi B(\nu, T) \label{eq: 16}\tag{16} \end{equation}However, because we have boundary conditions on $F^{\uparrow}$ and $F^{\downarrow}$ at the top of the atmosphere, it is convenient to redefine $\tau$ so that it is zero at the top of the atmosphere. We do this through: $\tau = \tau^{\infty} - \tau$. Subbing this into $(14)$ and $(16)$ gives us:
\begin{equation} \frac{dF^{\uparrow}_{\nu}}{d\tau_{\nu}} = F^{\uparrow}_{\nu} - \pi B(\nu, T) \label{eq: 17}\tag{17} \end{equation}\begin{equation} \frac{dF^{\downarrow}_{\nu}}{d\tau_{\nu}} = -F^{\downarrow}_{\nu} + \pi B(\nu, T) \label{eq: 18}\tag{18} \end{equation}The flux we need to sub into $(11)$ is $F = \int F^{\uparrow}_{\nu} d\nu - \int F^{\downarrow}_{\nu} d\nu$. So we need to know the frequency dependence of $\tau_{\nu}$. In the grey gas approximation, we assume that $k_{\nu}$ and thus $\tau_{\nu}$ doesn't depend on frequency. Integrating $(17)$ and $(18)$ over all frequencies then gives:
\begin{equation} \frac{dF^{\uparrow}}{d\tau} = F^{\uparrow} - \sigma_B T^4 \label{eq: 19}\tag{19} \end{equation}\begin{equation} \frac{dF^{\downarrow}}{d\tau} = -F^{\downarrow} + \sigma_B T^4 \label{eq: 20}\tag{20} \end{equation}where $\sigma_B$ is the Boltzmann constant. The solution to $(19)$ is given by:
\begin{equation} F^{\uparrow}(\tau) = F^{\uparrow}(\tau = 0)e^{\tau} + \int_0^{\tau} \sigma_B T(\tau')^4e^{\tau - \tau'} d\tau' \label{eq: 21}\tag{21} \end{equation}In the model, we find the flux at altitude level $i-1$, corresponding to $p + \Delta p$ and $\tau +\Delta \tau$ from the flux at paltitude level $i$, corresponding to $p$ and $\tau$. The equation for this is given by:
\begin{equation} F^{\uparrow}_{i} = F^{\uparrow}_{i-1}e^{-\Delta \tau} - \int_{\tau}^{\tau + \Delta \tau} \sigma_B T(\tau')^4e^{\tau - \tau'} d\tau' \label{eq: 22}\tag{22} \end{equation}We simplify this, by assuming that $T$ is constant between $\tau$ and $\tau + \Delta \tau$, so we take it out the integral and give it the value at $i-\frac{1}{2}$. So we finally get:
\begin{equation} F^{\uparrow}_{i-1} = F^{\uparrow}_{i}e^{\Delta \tau} + \sigma_B T_{i-1/2}^4(1 - e^{\Delta \tau}) \label{eq: 23}\tag{23} \end{equation}And similarly, for downwelling:
\begin{equation} F^{\downarrow}_{i-1} = F^{\downarrow}_{i}e^{-\Delta \tau} + \sigma_B T_{i-1/2}^4(1 - e^{-\Delta \tau}) \label{eq: 24}\tag{24} \end{equation}We can then compute $F_i = F^{\uparrow}_{i} - F^{\downarrow}_{i}$ and sub into $(11)$.
The boundary conditions are $F^{\uparrow}(\tau = 0) = (1-A)F^{\odot}/4$ and $F^{\downarrow}(\tau = 0) = 0$. $F^{\odot} = 1367 Wm^{-2}$ is the solar constant and $A$ is the albedo, indicating how much of the solar radiation is reflected, a typical value is 0.3. The first boundary conditions basically states that the radiation emitted by the planet into space must balance the radiation incoming into the planet from the star. The factor of 4 is because the sunlight only impinges on an area $\pi R_{Earth}^2$ whereas the planet radiates from its entire area $4\pi R_{Earth}^2$. The second boundary condition states that the star does not provide any radiation in the frequency range emitted by the planet (planet is cooler than star so frequency spectrum is at much lower frequency). The fact that both boundary conditions are at the top of the atmosphere is the reason why equations $(23)$ and $(24)$ are written such that the flux at level i-1 is calculated from the flux at level i (not sure this makes physical sense for the upward flux though!!).
Equations $(19)$ to $(24)$ are derived assuming the atmosphere only interacts with the long wavelength radiation emitted by the planet and by the atmosphere itself and not with the short wave radiation coming from the star.
To a first approximation, we can include the interaction with short wave radiation by instead of integrating $(17)$ and $(18)$ over all frequencies, but over two separate frequency regimes.
We get the short wave ($sw$) fluxes by integrating over the solar spectrum, so from about $\nu(\lambda = 0.1 \mu m)$ to $\nu(\lambda = 3 \mu m)$ as indicated above. We get the long wave ($lw$) fluxes by integrating over the eart spectrum, so from about $\nu(\lambda = 3 \mu m)$ to $\nu(\lambda = 100 \mu m)$. To do this we have to integrate over $B(\nu, T)$ in the two regimes. But the temperature in $B(\nu, T)$ is the temperature of a thin layer of the atmosphere and we expect this to be approximately the surface temperature of the planet and much smaller than the temperature of the star. Hence we can say:
\begin{equation} \int_{\nu(\lambda = 3 \mu m)}^{\infty} B(\nu, T) d\nu \approx 0 \label{eq: 25}\tag{25} \end{equation}\begin{equation} \int_{0}^{\nu(\lambda = 3 \mu m)} B(\nu, T) d\nu \approx \int_{0}^{\infty} B(\nu, T) d\nu = \sigma_B T^4 \label{eq: 26}\tag{26} \end{equation}And our four equations are:
\begin{equation} \frac{dF^{\uparrow}_{lw}}{d\tau_{lw}} = F^{\uparrow}_{lw} - \sigma_B T^4 \label{eq: 27}\tag{27} \end{equation}\begin{equation} \frac{dF^{\downarrow}_{lw}}{d\tau_{lw}} = -F^{\downarrow}_{lw} + \sigma_B T^4 \label{eq: 28}\tag{28} \end{equation}\begin{equation} \frac{dF^{\uparrow}_{sw}}{d\tau_{sw}} = F^{\uparrow}_{sw} \label{eq: 29}\tag{29} \end{equation}\begin{equation} \frac{dF^{\downarrow}_{sw}}{d\tau_{sw}} = -F^{\downarrow}_{sw} \label{eq: 30}\tag{30} \end{equation}The boundary conditions for the short wave equations are $F^{\uparrow}_{sw}(\tau = \tau_s) = AF^{\downarrow}_{sw}(\tau = \tau_s) = AF^{\odot}e^{-\tau_s}/4$ and $F^{\downarrow}_{sw}(\tau = 0) = F^{\odot}/4$. This is to satisfy the amount of solar light reflected by the planet and the flux of stellar light that reaches the planet respectively. Using these, we get:
\begin{equation} F^{\uparrow}_{sw} = \frac{1}{4}AF^{\odot}e^{\tau_{sw}-2\tau_{s}} \label{eq: 31}\tag{31} \end{equation}\begin{equation} F^{\downarrow}_{sw} = \frac{1}{4}F^{\odot}e^{-\tau_{sw}} \label{eq: 32}\tag{32} \end{equation}The $F$ value to sub into $(11)$ in this case is $F = F^{\uparrow}_{lw} + F^{\uparrow}_{sw} - F^{\downarrow}_{lw} - F^{\downarrow}_{sw}$.
The key here is that $(27)-(28)$ and $(29)-(30)$ depend on different optical depths. So if an atmosphere had no gas that interacted with stellar light, $\tau_{sw}$ would be zero everywhere and $F_{sw}$ would be constant everywhere and just provide a boundary condition for $F_{lw}$ as in equations $(19)-(24)$ i.e. $\frac{\partial F_{sw}}{\partial p} = 0$ so $F_{sw}$ does not appear in equation $(10)$. But if an atmosphere did have a gas that interacted with stellar light, $\tau_{sw}$ would be positive at the surface and thus from $(32)$, it is clear that less radiation will reach the surface causing a cooling effect there.
As in equation $(11)$, we have the flux at pressure level $i$, $F_i$, and all the flux equations are in terms of optical depth, $\tau$, we need a means of relating $\tau$ to pressure. The model gives four options in the file grey_optical_depth.py:
scale_height¶This is probably the most physical and is the form used in Atmospheric Circulation Dynamics and General Circulation Models. The form of the function is:
\begin{equation} \tau = \tau_s \left(\frac{p}{p_s}\right)^{\alpha + 1} \label{eq: 33}\tag{33} \end{equation}Where $\tau_s$ is the optical depth at the surface and $p_s$ is the pressure at the surface which is about 101320 Pa for Earth. The parameter $\alpha$, determines how sharply $\tau$ rises near the surface, a larger value means a steeper rise. $\alpha$ can be explained in more physical terms by considering the corresponding mass concentration of absorbing constituent profile:
\begin{equation} q = q_s \left(\frac{p}{p_s}\right)^{\alpha} = q_s e^{-z/h} \label{eq: 34}\tag{34} \end{equation}Where $q_s$ is the value at the surface and the $q_s e^{-z/h}$ is valid in an isothermal atmosphere, in which case $h = H / \alpha$ where $h$ is the scale height of the mass concentration and $H$ is the pressure scale height.
The function is called using OpticalDepthFunctions.scale_height(p, p_width, tau_surface)[1], where the first argument is the pressure levels where you would like $\tau$ to be computed. p_width is the difference between the pressure level at the surface and the pressure level where mass concentration, $q$ falls to $q_s/e$. $\alpha$ is then computed from p_width. Note that all the optical depth functions have multiple outputs, hence we only take the second output here.
Some examples with changing p_width and tau_surface = 4 is shown on the left in the cell below while examples with changing tau_surface and p_width = 10000 are shown on the right.
# Import everything needed in notebook
import Model.radiation.grey_optical_depth as od
from Model.radiation.grey import GreyGas
from Model.constants import p_surface_earth, p_toa_earth;
from Model.radiation.animation import Animate;
import numpy as np
import matplotlib.pyplot as plt
from IPython.display import HTML
import os
import contextlib
/Users/joshduffield/miniforge3/envs/ClimateModel/lib/python3.8/site-packages/sympl/_core/dataarray.py:6: FutureWarning: xarray subclass DataArray should explicitly define __slots__ class DataArray(xr.DataArray):
# scale_heigth optical depth plots
p = np.logspace(np.log10(p_surface_earth), np.log10(p_toa_earth), 500)
p_width_list = [5000, 10000, 50000] # Pa
tau_surface_list = [1,4, 6]
fig, axs = plt.subplots(1, 2, sharey=True)
for p_width in p_width_list:
tau = od.scale_height(p, p_width, tau_surface_list[1])[1]
axs[0].plot(tau, p, label = str(p_width))
for tau_surface in tau_surface_list:
tau = od.scale_height(p, p_width_list[1], tau_surface)[1]
axs[1].plot(tau, p, label = str(tau_surface))
axs[0].invert_yaxis()
axs[0].legend(title=r'$p_{width}$')
axs[1].legend(title=r'$\tau_s$')
axs[0].set_xlabel(r'$\tau$')
axs[1].set_xlabel(r'$\tau$')
axs[0].set_ylabel('Pressure / Pa')
fig.suptitle(r'scale_height $\tau$', fontsize = 14);
exponential¶The form of this is:
\begin{equation} \tau = \tau_s \frac{e^{\alpha p} - 1}{e^{\alpha p_s} - 1} \label{eq: 35}\tag{35} \end{equation}It has the same arguments as [scale_height], so it is called through OpticalDepthFunctions.exponential(p, p_width, tau_surface)[1]. It gives a very similar profile, as shown in the cell below for p_width = 10000 and tau_surface = 4. The reason for including this function is explained in the equilibrium solutions section.
fig, ax = plt.subplots(1, 1)
tau_scale_height = od.scale_height(p, p_width_list[1], tau_surface_list[1])[1]
tau_exponential = od.exponential(p, p_width_list[1], tau_surface_list[1])[1]
ax.plot(tau_scale_height, p, label = 'scale_height')
ax.plot(tau_exponential, p, label = 'exponential')
ax.invert_yaxis()
# ax.set_yscale('log')
ax.legend()
ax.set_xlabel(r'$\tau$')
ax.set_ylabel('Pressure / Pa')
fig.suptitle(r'exponential $\tau$', fontsize = 14);
peak_in_atmosphere¶The form of this is:
\begin{equation} \tau= \begin{cases} \tau_s \frac{e^{\alpha(p-p_{max})} - e^{-\alpha p_{max}}}{2 - e^{-\alpha p_{max}} - e^{\alpha (p_{max}-p_s)}} & p\leq p_{max}\\ \tau_s \frac{2 - e^{-\alpha p_{max}} - e^{\alpha (p_{max}-p)}}{2 - e^{-\alpha p_{max}} - e^{\alpha (p_{max}-p_s)}} & p > p_{max} \end{cases} \label{eq: 36}\tag{36} \end{equation}This looks complicated, but it is derived from a simple mass concentration distribution that is peaked somewhere other than the surface:
\begin{equation} q = q(p_{max})e^{-\alpha |p-p_{max}|} \label{eq: 37}\tag{37} \end{equation}The function is called through OpticalDepthFunctions.peak_in_atmosphere(p, p_width, p_max, tau_surface)[1]. p_max is the pressure level where the mass concentration distribution is peaked and now p_width is the difference between p_max and the pressure level where mass concentration, $q$ falls to $q(p_{max})/e$.
The cell below shows both the $\tau$ and $q$ distributions for p_max = 50000, tau_surface = 4 and varying p_width. Specific humidity, $q$ (see just above equation $(13)$) was derived assuming that the absoprtion coefficient, $k=1 m^2kg^{-1}$.
p_max = 50000
fig, axs = plt.subplots(1, 2, sharey=True)
for p_width in p_width_list:
q, tau, _, _ = od.peak_in_atmosphere(p, p_width, p_max, tau_surface_list[1], k=1)
axs[0].plot(tau, p, label = str(p_width))
axs[1].plot(q, p, label = str(p_width))
axs[0].invert_yaxis()
axs[0].legend(title=r'$p_{width}$')
axs[1].legend(title=r'$p_{width}$')
axs[0].set_xlabel(r'$\tau$')
axs[1].set_xlabel(r'$q$')
axs[0].set_ylabel('Pressure / Pa')
fig.suptitle(r'peak_in_atmosphere $\tau$', fontsize = 14);
scale_height_and_peak_in_atmosphere¶This is just the addition of scale_height with peak_in_atmosphere. It is called through OpticalDepthFunctions.scale_height_and_peak_in_atmosphere(p, p_width1, tau_surface1, p_width2, p_max2, tau_surface2)[1], where the arguments ending 1 correspond to scale_height and those ending 2 correspond to peak_in_atmosphere.
The below cell shows the $\tau$ and $q$ distributions for p_width1 = 1000, tau_surface1 = 4, p_width2 = 5000, p_max2 = 50000, tau_surface2 = 1.
fig, axs = plt.subplots(1, 2, sharey=True)
q, tau, _, _ = od.scale_height_and_peak_in_atmosphere(p, p_width_list[1], tau_surface_list[1],
p_width_list[0], p_max, tau_surface_list[0], k=1)
axs[0].plot(tau, p)
axs[1].plot(q, p)
axs[0].invert_yaxis()
axs[0].set_xlabel(r'$\tau$')
axs[1].set_xlabel(r'$q$')
axs[0].set_ylabel('Pressure / Pa')
fig.suptitle(r'scale_height_and_peak_in_atmosphere $\tau$', fontsize = 14);
Equilibrium is reached when $\frac{\partial T}{\partial t} = 0$ so from $(10)$, we require $\frac{\partial F}{\partial p} = 0$ and hence $F$ is a constant and in the absence of heat supplied by the Earth through e.g. radioactive decay, we take the constant to be zero. So we have the four equations $(27) - (30)$ aswell as $F = F^{\uparrow}_{lw} + F^{\uparrow}_{sw} - F^{\downarrow}_{lw} - F^{\downarrow}_{sw} = 0$. That gives us 5 equations in 5 unknowns which we can solve.
In the absence of any short wave interacting gas, $\tau_{sw} = 0$ and it becomes a lot simpler. The solution is below, where $F_{sw, eqb} = F^{\downarrow}_{sw, eqb} - F^{\uparrow}_{sw, eqb} = (1-A)F^{\odot}/4$: \begin{equation} F^{\uparrow}_{lw, eqb} = \frac{1}{2}F_{sw, eqb}(2 + \tau_{lw})\\ F^{\downarrow}_{lw, eqb} = \frac{1}{2}F_{sw, eqb}\tau_{lw}\\ T_{eqb} = \left[\frac{F_{sw, eqb}}{2\sigma_B}(1+\tau_{lw})\right]^{\frac{1}{4}} \label{eq: 38}\tag{38} \end{equation}
The below cell shows how you can view the analytic solution. First you need to create an instance of the GreyGas class by providing the optical depth function aswell as the required arguments. You also need to provide the number of grid points in pressure space, nz. If this is set to auto, the number of grid points will be chosen so as to get good coverage in both pressure and optical depth space through the function get_p_and_tau_interface_grids.
The equilibrium solution is then obtained through the function equilibrium_sol and is plotted through plot_eqb.
In the plot on the right, the negative fluxes correspond to $F^{\downarrow}$ while the positive fluxes correspond to $F^{\uparrow}$.
p_width_lw = 100000
tau_surface_lw = 4
lw_gas = GreyGas(nz='auto', ny=1, tau_lw_func=od.exponential, tau_lw_func_args=[p_width_lw, tau_surface_lw])
up_flux_eqb, down_flux_eqb, T_eqb_lw, up_sw_flux_eqb, down_sw_flux_eqb, correct_solution_lw = lw_gas.equilibrium_sol()
lw_gas.plot_eqb(up_flux_eqb, down_flux_eqb, T_eqb_lw, up_sw_flux_eqb, down_sw_flux_eqb)
The solution to the 5 equations is a lot more complicated if $\tau_{sw} \neq 0$. First, you need to express all the variables in terms of one of the parameters $p$, $\tau_{lw}$ or $\tau_{sw}$. It is easiest if we use $\tau_{sw}$, and we end up having to compute the following integral, where $A$ is the albedo:
\begin{equation} \int_{0}^{\tau_{sw}} \frac{d\tau_{lw}}{d\tau_{sw}'} (e^{-\tau_{sw}'} - Ae^{\tau_{sw}'}) d\tau_{sw}' \label{eq: 39}\tag{39} \end{equation}To get a solution to this, we require $\frac{d\tau_{lw}}{d\tau_{sw}'}$ is of the form $\sum_n (\tau_{sw}')^n$ where n is always an integer. The only way we can do this using our OpticalDepthFunctions is if both $\tau_{lw}$ and $\tau_{sw}$ are exponential and the ratio $\frac{\alpha_{lw}}{\alpha_{sw}}$ is a small integer (< 10, theoretically a solution is possible for larger integers but it takes a long time to compute).
If the optical depth functions supplied do not satisfy this requirement, the returned solution will be that with $\tau_{sw} = 0$ and the value of correct_solution will be set to False to indicated the wrong equilibrium has been calculated.
The below cell shows the equilibrium solution obtained by adding a short wave component to the lw_gas in the above cell. To ensure that the $\alpha$ ratio is an integer, we first need to find $\alpha_{lw}$ using get_exponential_alpha, then divide this by an integer before finding the corresponding $p_{width}$ using get_exponential_p_width.
In these plots, the dotted lines show the solution, where $\tau_{sw} = 0$.
It can be seen from this plot that the effect of adding the short wave component is to decrease the temperature at the surface and increase the temperature in the higher atmosphere. The surface temperature decreases because the short wave gas absorbs sunlight, reducing the amount that reaches the surface, as shown by the downward $F_{sw}$ in the plot on the right. This explains why the temperature of the sea decreases as you go deeper. The increase in temperature higher up is because the upward flux of short wave radiation has increased.
Physically, you can think that the short wave gas is heated by sunlight so if the short wave gas is dominant, the temperature will increase as you move to the top of the atmosphere. Long wave gas is heated by terrestrial light so if long wave gas is dominant, the temperature will increase as you move towards the surface.
alpha_lw = od.get_exponential_alpha(p_width_lw)
alpha_sw = alpha_lw / 5
p_width_sw = od.get_exponential_p_width(alpha_sw)
tau_surface_sw = 0.6
sw_gas = GreyGas(nz='auto', ny=1, tau_lw_func=od.exponential, tau_lw_func_args=[p_width_lw, tau_surface_lw],
tau_sw_func=od.exponential, tau_sw_func_args=[p_width_sw, tau_surface_sw])
up_flux_eqb, down_flux_eqb, T_eqb_sw, up_sw_flux_eqb, down_sw_flux_eqb, correct_solution_sw = sw_gas.equilibrium_sol()
sw_gas.plot_eqb(up_flux_eqb, down_flux_eqb, T_eqb_sw, up_sw_flux_eqb, down_sw_flux_eqb)
We can use equation $(11)$ to investigate how temperature evolves with time if the initial state of the atmosphere was not in equilibrium.
The function to call to do this is evolve_to_equilibrium and it returns a dictionary, dict, where dict['t'] gives a list of all the times of each step of the simulation and dict['T'][i] gives the temperature profile at time dict['t'][i].
An example for lw_gas initialised in the previous section is shown below. The temperature is adjusted at each time step using the function take_time_step(t, T_initial). t is just the current time and T_initial is the starting temperature profile, and is only necessary if t=0. If T_initial is not provided, the initial profile will be an isothermal atmosphere such that $F^{\uparrow}_{lw} + F^{\uparrow}_{sw} = F^{\downarrow}_{sw}$ at the top of the atmosphere.
This function will find the fluxes corresponding to the current temperature profile and then automatically find a time step such that exactly one pressure level has a temperature change of 1 Kelvin and all other levels have a smaller temperature change. This time step is found through the function update_time_step and the max temperature change will be lowered from 1 Kelvin if convergence isn't happening.
take_time_step returns the time updated by the timestep aswell as delta_net_flux which indicates by how much the net flux, $F$, is changing. The idea being that when this value is small equilibrium has been reached. In fact, the functioncheck_equilibrium(delta_net_flux, flux_thresh) explicitly checks for equilibrium by returning True if delta_net_flux < flux_thresh.
The evolution of the temperature is plotted by getting an array of all the times and temperatures then running Animate(grey_world, T_array, t_array, T_eqb, correct_solution, log_axis=True, nPlotFrames=100) using the class in the animation.py file. This then finds nPlotFrames (100 by default) suitable times to plot from all those in T_array. The log_axis parameter determines whether to plot pressure on a log scale, it is set to True by default. T_eqb does not have to be supplied but helps check if the analytical solution is being approached.
# Start lw_gas at isothermal and evolve to equilibrium
data_lw = lw_gas.evolve_to_equilibrium(flux_thresh=0.1)
Trying to reach equilibrium (flux_thresh = 0.1000)... 0 Years, 150 Days: delta_net_flux = 0.0744 Done!
anim_lw = Animate(lw_gas, data_lw['T'], data_lw['t'], T_eqb_lw, correct_solution_lw).anim; plt.close();
HTML(anim_lw.to_jshtml())
We can do the same thing for the sw_gas initialised just below equation $(36)$, as given by the next cell, for which we also have a correct analytical solution.
# Start sw_gas at isothermal and evolve to equilibrium
data_sw = sw_gas.evolve_to_equilibrium(flux_thresh=0.1)
Trying to reach equilibrium (flux_thresh = 0.1000)... 0 Years, 55 Days: delta_net_flux = 0.0542 Done!
anim_sw = Animate(sw_gas, data_sw['T'], data_sw['t'], T_eqb_sw, correct_solution_sw).anim; plt.close();
HTML(anim_sw.to_jshtml())
If we don't require an analytic solution, we can obtain more interesting atmospheric structures. In the cell below, we include a gas that absorbs short wave radiation at a pressure level of 2,000 Pa. This then causes a temperature inversion, analagous to the role of ozone in our stratosphere.
Note that in the animation, the red curve doesn't converge to the orange curve because the orange solution is that with $\tau_{sw} = 0$ as indicated by the warning.
def plot_tau_and_q(gas, title, log_scale = True):
"""Function to show optical depth and mass concentration profiles"""
lw_color = '#ff7f0e'
sw_color = '#1f77b4'
fig, axs = plt.subplots(1, 2, sharey=True)
axs[0].plot(gas.tau, gas.p, color = lw_color)
axs[1].plot(gas.q, gas.p, label = 'lw', color = lw_color)
if not gas.sw_tau_is_zero:
axs[0].plot(gas.tau_sw, gas.p, color = sw_color)
axs[1].plot(gas.q_sw, gas.p, label = 'sw', color = sw_color)
axs[1].legend()
axs[0].invert_yaxis()
axs[0].set_xlabel(r'$\tau$')
axs[1].set_xlabel(r'$q$')
axs[0].set_ylabel('Pressure / Pa')
if log_scale:
axs[0].set_yscale('log')
axs[1].set_yscale('log')
fig.suptitle(title, fontsize = 14);
# Atmosphere with stratosphere
stratosphere = GreyGas(nz='auto', ny=1, tau_lw_func=od.exponential,
tau_lw_func_args=[100000, 4], tau_sw_func=od.peak_in_atmosphere,
tau_sw_func_args=[30000, 2000, 0.5])
_, _, T_eqb_stratosphere, _, _, correct_solution_stratosphere = stratosphere.equilibrium_sol()
plot_tau_and_q(stratosphere, 'Stratosphere', True)
/Users/joshduffield/Documents/PlanetaryClimate/ClimateModel/Model/radiation/grey.py:426: UserWarning:
Can only compute exact solution if both long wave and short wave optical depth functions are exponential.
Selected functions are:
Long wave - exponential
Short Wave - peak_in_atmosphere
Returned equilibrium solution is that with short wave optical depth = 0 everywhere.
warnings.warn("\nCan only compute exact solution if both long wave and short wave optical depth functions"
# Start stratosphere gas at isothermal and evolve to equilibrium
data_stratosphere = stratosphere.evolve_to_equilibrium(flux_thresh=0.1)
Trying to reach equilibrium (flux_thresh = 0.1000)... 0 Years, 53 Days: delta_net_flux = 0.0950 Done!
anim_stratosphere = Animate(stratosphere, data_stratosphere['T'], data_stratosphere['t'],
T_eqb_stratosphere, correct_solution_stratosphere).anim; plt.close();
HTML(anim_stratosphere.to_jshtml())
The next example shows that if we include a peak in the mass concentration of the long wave gas above the peak of the short wave gas, we can construct an atmosphere which has a mesosphere where there is another temperature inversion.
In this example, we also track the fluxes at each step of the simulation. We do this by initialising the dictionary to pass to evolve_to_equilibrium. We must provide the first entry for each key of the dictionary. The data corresponding to the flux key is also a dictionary containing the up/down long-wave/short-wave permutations.
import warnings
warnings.filterwarnings('ignore') # to ignore can't find equilibrium sol warning
mesosphere = GreyGas(nz='auto', ny=1, tau_lw_func=od.scale_height_and_peak_in_atmosphere,
tau_lw_func_args=[50000, 4, 1000, 600, 0.3], tau_sw_func=od.peak_in_atmosphere,
tau_sw_func_args=[10000, 2000, 0.05]);
_, _, T_eqb_mesosphere, _, _, correct_solution_mesosphere = mesosphere.equilibrium_sol();
plot_tau_and_q(mesosphere, 'Mesosphere', True)
# Start mesosphere gas at isothermal and evolve to equilibrium
flux_array = {'lw_up': [mesosphere.up_lw_flux.copy()], 'lw_down': [mesosphere.down_lw_flux.copy()],
'sw_up': [mesosphere.up_sw_flux.copy()], 'sw_down': [mesosphere.down_sw_flux.copy()]}
data_mesosphere = {'t': [0], 'T': [mesosphere.T.copy()],'flux': flux_array}
data_mesosphere = mesosphere.evolve_to_equilibrium(data_mesosphere, flux_thresh=0.1)
Trying to reach equilibrium (flux_thresh = 0.1000)... 0 Years, 42 Days: delta_net_flux = 0.0884 Done!
anim_mesosphere = Animate(mesosphere, data_mesosphere['T'], data_mesosphere['t'], T_eqb_mesosphere,
correct_solution_mesosphere, flux_array=data_mesosphere['flux']).anim; plt.close();
HTML(anim_mesosphere.to_jshtml())
Finally, if we include a short wave gas which has pretty much constant non-zero concentration except for a peak at a level above the mesosphere peak in long wave gas concentration, we can get a thermosphere, where the temperature increases as you move out to space.
thermosphere = GreyGas(nz='auto', ny=1, tau_lw_func=od.scale_height_and_peak_in_atmosphere,
tau_lw_func_args=[51000, 4, 100, 600, 0.1], tau_sw_func=od.scale_height_and_peak_in_atmosphere,
tau_sw_func_args=[p_surface_earth, 0.12, 100, 20, 0.002])
_, _, T_eqb_thermosphere, _, _, correct_solution_thermosphere = thermosphere.equilibrium_sol()
plot_tau_and_q(thermosphere, 'Thermosphere', True)
# Start thermosphere gas at isothermal and evolve to equilibrium
flux_array_thermosphere = {'lw_up': [thermosphere.up_lw_flux.copy()], 'lw_down': [thermosphere.down_lw_flux.copy()],
'sw_up': [thermosphere.up_sw_flux.copy()], 'sw_down': [thermosphere.down_sw_flux.copy()]}
data_thermosphere = {'t': [0], 'T': [thermosphere.T.copy()],'flux': flux_array_thermosphere}
data_thermosphere = thermosphere.evolve_to_equilibrium(data_thermosphere, flux_thresh=0.1)
Trying to reach equilibrium (flux_thresh = 0.1000)... 0 Years, 47 Days: delta_net_flux = 0.0982 Done!
anim_thermosphere = Animate(thermosphere, data_thermosphere['T'], data_thermosphere['t'],
T_eqb_thermosphere, correct_solution_thermosphere, flux_array=data_thermosphere['flux']).anim; plt.close();
HTML(anim_thermosphere.to_jshtml())
We can also include convective adjustment which ensures that $dT/dz \geq -\gamma$ where $\gamma$ is the critical lapse rate, which by default is the dry adiabatic lapse rate, $\gamma = g/c_p$.
This is done by setting convective_adjust=True in evolve_to_equilibrium, take_time_step or equilibrium_sol.
The below shows a comparison between the final temperature profile for the thermosphere example above when convection is and is not included. You can see that in the locations where the temperature is increasing with increasing with increasing pressure (around $10^3 Pa$ and above $2 \times 10^4 Pa$), the rate of increase in the convective case is reduced.
The convective adjustment algorithm is explained in Convective Adjustment.ipynb.
thermosphere_convective = GreyGas(nz='auto', ny=1, tau_lw_func=od.scale_height_and_peak_in_atmosphere,
tau_lw_func_args=[51000, 4, 100, 600, 0.1],
tau_sw_func=od.scale_height_and_peak_in_atmosphere,
tau_sw_func_args=[p_surface_earth, 0.12, 100, 20, 0.002])
data_thermosphere_conv = {'t': [0], 'T': [thermosphere_convective.T.copy()]}
data_thermosphere_conv = thermosphere_convective.evolve_to_equilibrium(data_thermosphere_conv, flux_thresh=0.1, convective_adjust=True)
Trying to reach equilibrium (flux_thresh = 0.1000)... 0 Years, 40 Days: delta_net_flux = 0.0990 Done!
fig, ax = plt.subplots(1,1)
ax.plot(data_thermosphere['T'][-1], thermosphere.p, label='No Convection')
ax.plot(data_thermosphere_conv['T'][-1], thermosphere.p, label='With Convection')
ax.invert_yaxis()
ax.set_yscale('log')
ax.legend();
ax.set_ylabel('Pressure / Pa');
ax.set_xlabel('Temperature / K');
ax.set_title('Equilibrium Temperature Profile');
We can also see how the Temperature profile changes in response to changes in the optical depth profile.
In this example, we start off with just a long wave gas. We then increase the surface optical thickness with time until it reaches 6. At this stage we then let the atmosphere reach equilibrium. Once equilibrium is reached, we introduce a short wave gas peaked at 20000 Pa. We increase the short wave surface optical depth with time until it reaches 1.2. Then we let the atmosphere reach its new equilibrium. Finally, we remove all the short wave gas so the optical depth goes to 0 instantly and see the final equilibrium reached.
The idea behind this simulation is the geoengineering solution to global warming of inject aerosols into the stratosphere. So the initial stage indicates global warming where more greenhouse gases increase the surface temperature. The next step is the injection of aerosols which interact with short wave radiation and we can see from the animation below that the surface temperature starts decreasing when the short wave optical depth is non-zero. The final removal of short wave optical depth is included because aerosols have a short lifetime so if future generations stop injecting them, this is what will happen. The animation shows that the surface temperature increased by global warming is recovered after the removal of aerosols.
To run this simulation, we first initialise the gas with the largest optical depth parameters that will be encountered in the simulation, indicated by the final suffix here. This is so the pressure grid has enough points in areas where there is a high mass concentration. We then set the optical depth parameters to their start values and update the optical depth grid values using the function update_grid.
We next find the analytical equilibrium solution for the starting optical depth and use this as the starting point for the simulation. In the data dictionary, we also include the optical depth because it is changing, so we want to add it to the animation.
When running the simulation, we call take_time_step(t, T_eqb.copy(), changing_tau) during times where the optical depth is changing. This is because we must include the fact that changing_tau=True so the function recomputes the optical depths at each time. At times when we just want to reach equilibrium, we just run evolve_to_equilibrium, providing the current state of the data dictionary. We stop the simulation either after 10 years have passed or we have reached the final equilibrium after the aerosols have been removed.
def aerosols_example(ny, tau_sw_final=1.2):
tau_lw_params_final = [100000, 6]
tau_lw_params = [100000, 4]
tau_sw_params_final = [300000, 2000, tau_sw_final]
tau_sw_params = [300000, 2000, 0]
aerosol = GreyGas(nz='auto', ny=ny, tau_lw_func=od.exponential, tau_lw_func_args=tau_lw_params_final,
tau_sw_func=od.peak_in_atmosphere, tau_sw_func_args=tau_sw_params_final)
aerosol.tau_lw_func_args = tuple(tau_lw_params)
aerosol.tau_sw_func_args = tuple(tau_sw_params)
aerosol.update_grid()
up_flux_eqb, down_flux_eqb, T_eqb, up_sw_flux_eqb, down_sw_flux_eqb, \
correct_solution = aerosol.equilibrium_sol()
t = 0
t_end = 10 * 365 * 24 * 60 ** 2
t_sw = t_end
t_array = [0]
T_array = [T_eqb.copy()]
tau_array = {'lw': [aerosol.tau.copy()], 'sw': [aerosol.tau_sw.copy()]}
flux_array = {'lw_up': [up_flux_eqb.copy()], 'lw_down': [down_flux_eqb.copy()],
'sw_up': [up_sw_flux_eqb.copy()], 'sw_down': [down_sw_flux_eqb.copy()]}
data = {'t': t_array, 'T': T_array, 'tau': tau_array, 'flux': flux_array}
changing_tau = True
delta_net_flux_thresh = 1e-3
while t < t_end:
tau_lw_params[1] = min(tau_lw_params[1] + 1e-8 * t, tau_lw_params_final[1])
aerosol.tau_lw_func_args = tuple(tau_lw_params)
if tau_lw_params[1] == tau_lw_params_final[1] and tau_sw_params[2] != tau_sw_params_final[2]:
if t_sw == t_end:
# once lw optical depth reached max value, get to equilibrium
data = aerosol.evolve_to_equilibrium(data, delta_net_flux_thresh, T_eqb.copy())
t = data['t'][-1]
t_sw = t
# After equilibrium evolve sw optical depth
tau_sw_params[2] = min(tau_sw_params[2] + 1e-4 * (t - t_sw) / aerosol.time_step_info['dt'],
tau_sw_params_final[2])
aerosol.tau_sw_func_args = tuple(tau_sw_params)
if tau_sw_params[2] == tau_sw_params_final[2]:
# once sw optical depth reached max value, get to equilibrium
data = aerosol.evolve_to_equilibrium(data, delta_net_flux_thresh, T_eqb.copy())
# set sw optical depth to zero and then see how it evolves from there
tau_sw_params[2] = 0
aerosol.tau_sw_func_args = tuple(tau_sw_params)
aerosol.update_grid()
data = aerosol.evolve_to_equilibrium(data, delta_net_flux_thresh, T_eqb.copy())
t = t_end + 10 # once reached final equilibrium, end simulation
else:
t = aerosol.take_time_step(t, T_eqb.copy(), changing_tau)[0]
data = aerosol.save_data(data, t)
return aerosol, data, T_eqb, correct_solution
aerosol_single, data, T_eqb, correct_solution = aerosols_example(1)
Trying to reach equilibrium (flux_thresh = 0.0010)... 0 Years, 65 Days: delta_net_flux = 0.00040 Done! Trying to reach equilibrium (flux_thresh = 0.0010)... 0 Years, 249 Days: delta_net_flux = 0.00050 Done! Trying to reach equilibrium (flux_thresh = 0.0010)... 1 Years, 88 Days: delta_net_flux = 0.0009189 Done!
anim_aerosol = Animate(aerosol_single, data['T'], data['t'], T_eqb, correct_solution, data['tau'], data['flux']).anim; plt.close();
HTML(anim_aerosol.to_jshtml())
This second example shows the result of increasing the surface $\tau_{lw}$ from 4 to 8 for the thermosphere example. Basically, the simulation achieves equilibrium with the current $\tau$ parameters and then increases tau_lw_surface by 0.1 and repeats.
The starting profile is the profile that the previous thermosphere simulation ended on and this is the temperature profile shown in orange on the left plot. I think the initial increase in mesopause temperature is because the convergence threshold of delta_net_flux_thresh = 1e-3 is much stricter here. After this start though, you can see a trend for surface temperature to increase, mesopause to narrow and its temperature to decrease and tropopause to shift up.
The narrowing occurs because there is very little interaction of long radiation with gas above $\tau_{lw}=1$ so to match the boundary conditions at the top of the atmosphere, the radiation coming from the $\tau_{lw}=1$ level must be constant and thus so must the temperature according to $\sigma_B T^4$. So when the $\tau_{lw}=1$ level moves to a lowe pressure, the temperature profile has to shift up aswell. This consequently pushes the tropopause closer to the mesopause.
In other words, as optical depth increases, the highest pressure value where the blue line intersects the red line moves to a lower pressure. To intersect at this reduced pressure level, the surface temperature has to increase. I wonder whether it could somehow jump though so that the emitting pressure level is the intersection in the stratosphere instead of in the troposphere, in which case it could be achieved without a change in surface temperature.
I tried to investigate this by, increasing the long wave gas concentration in the upper atmosphere in the cell after this. There are some big fluctuations in temperature so I am not sure if it is reliable but it shows that there is now only one intersection between the red and blue lines and this is very high in the atmosphere. So now it is not the temperature profile near the surface that is important to satisfy the top of atmosphere boundary condition but the profile near the top of the atmosphere.
tau_sw_params = [p_surface_earth, 0.12, 100, 20, 0.002]
tau_lw_params = [51000, 4, 100, 600, 0.1]
tau_lw_params_final = [51000, 8, 100, 600, 0.1]
t_array = [0]
T_array = [thermosphere.T.copy()]
tau_array = {'lw': [thermosphere.tau.copy()], 'sw': [thermosphere.tau_sw.copy()]}
flux_array = {'lw_up': [thermosphere.up_lw_flux.copy()], 'lw_down': [thermosphere.down_lw_flux.copy()],
'sw_up': [thermosphere.up_sw_flux.copy()], 'sw_down': [thermosphere.down_sw_flux.copy()]}
data = {'t': t_array, 'T': T_array, 'tau': tau_array, 'flux': flux_array}
changing_tau = True
delta_net_flux_thresh = 1e-3
keep_going = True
# too much info to print out
with open(os.devnull, "w") as f, contextlib.redirect_stdout(f):
while keep_going:
data = thermosphere.evolve_to_equilibrium(data, delta_net_flux_thresh)
tau_lw_params[1] = tau_lw_params[1] + 0.1
if tau_lw_params[1] > tau_lw_params_final[1]:
keep_going = False
else:
thermosphere.tau_lw_func_args = tuple(tau_lw_params)
thermosphere.update_grid()
anim = Animate(thermosphere, data['T'], data['t'], tau_array=data['tau'], flux_array=data['flux'],
fract_frames_at_start=0).anim; plt.close();
HTML(anim.to_jshtml())
tau_sw_params = [p_surface_earth, 0.12, 100, 20, 0.002]
tau_lw_params = [51000, 8, 100, 600, 0.1]
tau_lw_params_final = [51000, 8, 100, 600, 1.5]
t_array = [0]
T_array = [thermosphere.T.copy()]
tau_array = {'lw': [thermosphere.tau.copy()], 'sw': [thermosphere.tau_sw.copy()]}
flux_array = {'lw_up': [thermosphere.up_lw_flux.copy()], 'lw_down': [thermosphere.down_lw_flux.copy()],
'sw_up': [thermosphere.up_sw_flux.copy()], 'sw_down': [thermosphere.down_sw_flux.copy()]}
data = {'t': t_array, 'T': T_array, 'tau': tau_array, 'flux': flux_array}
changing_tau = True
delta_net_flux_thresh = 1e-3
keep_going = True
# too much info to print out
with open(os.devnull, "w") as f, contextlib.redirect_stdout(f):
while keep_going:
data = thermosphere.evolve_to_equilibrium(data, delta_net_flux_thresh)
tau_lw_params[-1] = tau_lw_params[-1] + 0.1
if tau_lw_params[-1] > tau_lw_params_final[-1]:
keep_going = False
else:
thermosphere.tau_lw_func_args = tuple(tau_lw_params)
thermosphere.update_grid()
anim = Animate(thermosphere, data['T'], data['t'], tau_array=data['tau'], fract_frames_at_start=0).anim; plt.close();
HTML(anim.to_jshtml())
You can also choose to include a latitudinal dependence of the incoming solar radiation by setting ny>1. This will then make ny latitudes evenly spaced between $-90^{\circ}$ and $+90^{\circ}$. The flux that arrives at each latitude is determined from the annually averaged value as given by equation 13.2.6 in Atmospheric Circulation dynamics and General Circulation Models. The latitude columns do not interact and the default initial condition is so that each latitude column is isothermal and the incoming short wave radiation is equal to the outgoing long wave radiation as the top of the atmosphere. You can also specify the albedo to be different for different latitudes. At the moment, it assumes that the optical depth distribution is the same for each latitude column.
The animation shows the temperature as a function of pressure and latitude as well as the surface temperature as a function of latitude. You can also provide the optical depth but the flux cannot be plotted as there are too many values when considering each latitude.
The below example just repeats the aerosols in the stratosphere simulation but with 30 latitude bins.
aerosol_latitudes, data, _, _ = aerosols_example(30, tau_sw_final=1.0) # lower final tau_sw otherwise get negative temperatures
Trying to reach equilibrium (flux_thresh = 0.0010)... 0 Years, 68 Days: delta_net_flux = 0.00093 Done! Trying to reach equilibrium (flux_thresh = 0.0010)... 0 Years, 147 Days: delta_net_flux = 0.0009 Done! Trying to reach equilibrium (flux_thresh = 0.0010)... 0 Years, 204 Days: delta_net_flux = 0.000860 Done!
anim_aerosol_latitudes = Animate(aerosol_latitudes, data['T'], data['t'], tau_array=data['tau']).anim; plt.close();
HTML(anim_aerosol_latitudes.to_jshtml())